<div class="tmd-doc">
<p></p>
<h1 class="tmd-header-1">
<code class="tmd-code-span">for</code> Loop Semantic Changes in Go 1.22: Be Aware of the Impact
</h1>
<p></p>
<div class="tmd-usual">
Go 1.22 changed the semantics of <code class="tmd-code-span">for</code> loops, including both <code class="tmd-code-span">for-range</code> loops and traditional 3-clause <code class="tmd-code-span">for ..; ..; .. {...}</code> loops (which will be abbreviated as <code class="tmd-code-span">for;;</code> in the remaining content of this article).
</div>
<p></p>
<div class="tmd-usual">
You should understand the semantic changes and understand the implications of these changes in order to write Go code which will behave as intended. Otherwise, your code may exhibit unexpected behavior.
</div>
<p></p>
<div class="tmd-usual">
Contents:
</div>

<ul class="tmd-list tmd-toc">
<li class="tmd-list-item tmd-toc-item"><a href="#overview">What are the changes?</a>
</li>
<li class="tmd-list-item tmd-toc-item"><a href="#impact">The impact of the changes</a>
</li>

<ul class="tmd-list tmd-toc">
<li class="tmd-list-item tmd-toc-item"><a href="#defer-calls">The behaviors of deferred function calls which capture loop variables might change</a>
</li>
<li class="tmd-list-item tmd-toc-item"><a href="#capture-loop-vars">Be careful when capturing loop variables in closures</a>
</li>
<li class="tmd-list-item tmd-toc-item"><a href="#take-addresses-of-loop-vars">Be careful when taking addresses of loop variables</a>
</li>
<li class="tmd-list-item tmd-toc-item"><a href="#place-3rd-clauses">Be careful when moving the 3rd clause statements inside loop bodies</a>
</li>
<li class="tmd-list-item tmd-toc-item"><a href="#nocopy-loop-vars">Be careful when declaring no-copy values as loop variables</a>
</li>
<li class="tmd-list-item tmd-toc-item"><a href="#large-size-loop-vars">Warning: the performance of your Go programs might be degraded silently</a>
</li>
<li class="tmd-list-item tmd-toc-item"><a href="#sharing-loop-vars">Warning: things might become more subtle than before when loop variables are used concurrently</a>
</li>
</ul>
<li class="tmd-list-item tmd-toc-item"><a href="#advice">Advice and suggestions</a>
</li>

<ul class="tmd-list tmd-toc">
<li class="tmd-list-item tmd-toc-item"><a href="#file-go-versions">Specify Go language versions for Go source files</a>
</li>
<li class="tmd-list-item tmd-toc-item"><a href="#notes-during-upgrading">Upgrading module versions</a>
</li>
<li class="tmd-list-item tmd-toc-item"><a href="#avoid-freshly-declared-loop-vars">Avoid using freshly-declared loop variables in <code class="tmd-code-span">for;;</code> loops if you worry about getting bitten by the pitful of the new semantics</a>
</li>
</ul>
<li class="tmd-list-item tmd-toc-item"><a href="#final-words">Final words</a>
</li>
</ul>
<p></p>
<h2 id="overview" class="tmd-header-2">
What are the changes?
</h2>
<p></p>
<div class="tmd-usual">
Specifically speaking, only the semantics of the <code class="tmd-code-span">for</code> loops which loop variables are declared within the loops are changed (we call such loop variables as freshly-declared loop variables in the remaining content). For example, in the following piece of code, the semantics of the former two loops are not changed, but the latter two ones are changed (from Go 1.21 to 1.22).
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">	for k, v = range aContainer {...}
	for a, b, c = f(); condition; statement {...}

	for k, v := range aContainer {...}
	for a, b, c := f(); condition; statement {...}
</code></pre>
<p></p>
<div class="tmd-usual">
The former two loops don't declare their respective loop variables, but the latter two do. That is the difference here. The semantics of the former two loops are not changed.
</div>
<p></p>
<div class="tmd-usual">
Let's view a simple Go program which undergoes semantic change (and behavior change) from Go 1.21 to Go 1.22:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">//demo1.go
package main

func main() {
	c, out := make(chan int), make(chan int)

	m := map[int]int{1: 2, 3: 4}
	for i, v := range m {
		go func() {
			&lt;-c
			out &lt;- i+v
		}()
	}

	close(c)

	println(&lt;-out + &lt;-out)
}
</code></pre>
<p></p>
<div class="tmd-usual">
We can install multiple Go toolchain versions to check the outputs. Here, I use the GoTV tool <sup><a id="fn:gotv:ref-1" href="#fn:gotv">[1]</a></sup> to (conveniently) choose Go toolchain versions.
</div>
<p></p>
<div class="tmd-usual">
The outputs:
</div>
<pre class="tmd-code">
$ gotv 1.21. run demo1.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo1.go
14
$ gotv 1.22. run demo1.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo1.go
10
</pre>
<p></p>
<div class="tmd-usual">
The behavior difference is obvious:
</div>
<ul class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
prior to Go 1.22, it printed <code class="tmd-code-span">14</code> (very probably, when without the participation of the channel <code class="tmd-code-span">c</code>);
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
since Go 1.22, it prints <code class="tmd-code-span">10</code> (always, even without the participation of the channel <code class="tmd-code-span">c</code>).
</div>
</li>
</ul>
<p></p>
<div class="tmd-usual">
The reason for the difference:
</div>
<ul class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
prior to Go 1.22, every freshly-declared loop variable used in a <code class="tmd-code-span">for</code> loop is shared by all iterations during executing the loop. The two new created goroutines are executed after the execution of the <code class="tmd-code-span">for</code> loop, in which case, the final values of the <code class="tmd-code-span">i</code> and <code class="tmd-code-span">v</code> loop variables are <code class="tmd-code-span">3</code> and <code class="tmd-code-span">4</code>. <code class="tmd-code-span">(3+4) + (3+4)</code> gives <code class="tmd-code-span">14</code>.
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
since Go 1.22, every freshly-declared loop variable used in a <code class="tmd-code-span">for</code> loop will be instantiated as a distinctive instance at the start of each iteration. In other words, it is per-iteration scoped now. So the values of the <code class="tmd-code-span">i</code> and <code class="tmd-code-span">v</code> loop variables used in the two new created goroutines are <code class="tmd-code-span">1 2</code> and <code class="tmd-code-span">3 4</code>, respectively. <code class="tmd-code-span">(1+2) + (3+4)</code> gives <code class="tmd-code-span">10</code>.
</div>
</li>
</ul>
<p></p>
<div class="tmd-usual">
Prior to Go 1.22, <span class="tmd-bold">without the participation of the channel <code class="tmd-code-span">c</code></span>, there is a data race condition present in the above program code, which should be a clear fact for a competent Go programmer. In order to avoid data race and get the same result as the new semantics, the loop in the program should be re-written as:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">	for i, v := range m {
		i, v := i, v // this line is added
		go func() {
			out &lt;- i+v
		}()
	}
</code></pre>
<p></p>
<div class="tmd-usual">
Under the new semantics, the added line becomes unnecessary. In fact, this is the main reason why the semantic changes were made in Go 1.22.
</div>
<p></p>
<div class="tmd-usual">
Similarly, the following program also undergoes semantic/behavior change from Go 1.21 to Go 1.22:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo2.go
package main

func main() {
	c, out := make(chan int), make(chan int)

	for i := 1; i &lt;= 3; i++ {
		go func() {
			&lt;-c
			out &lt;- i
		}()
	}

	close(c)

	println(&lt;-out + &lt;-out + &lt;-out)
}
</code></pre>
<p></p>
<div class="tmd-usual">
The outputs of the above program:
</div>
<pre class="tmd-code">
$ gotv 1.21. run demo2.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo2.go
12
$ gotv 1.22. run demo2.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo2.go 
6
</pre>
<p></p>
<div class="tmd-usual">
This article focuses on the details of the changes and impact of the changes, rather than the reasons behind them. For details on the approval process and reasons behind the changes, see
</div>
<ol class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
The first discussion: redefining for loop variable semantics: <a href="https://github.com/golang/go/discussions/56010">https://github.com/golang/go/discussions/56010</a>.
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
The formal proposal issue thread: <a href="https://github.com/golang/go/issues/60078">https://github.com/golang/go/issues/60078</a>. <span class="tmd-bold">Note that the open comment of this thread almost didn't mention the semantic change on <code class="tmd-code-span">for;;</code> loops.</span>
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
The proposal file itself: <a href="https://go.googlesource.com/proposal/+/master/design/60078-loopvar.md">https://go.googlesource.com/proposal/+/master/design/60078-loopvar.md</a>. <span class="tmd-italic">(If you only care about the reasons, you can just read this one.)</span>
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
The wiki page:    <a href="https://go.dev/wiki/LoopvarExperiment">https://go.dev/wiki/LoopvarExperiment</a>, and the blog article: <a href="https://go.dev/blog/loopvar-preview">https://go.dev/blog/loopvar-preview</a>. Both of them were created after the proposal was accepted, which means <span class="tmd-bold">the proposal was accepted before the experiment phase even began!</span> <span class="tmd-italic">(It was very unusual. The experiment phase seemed like a mere formality. This was evident in the brevity of the experiment phase period and the lack of a strong public call for participation. Consequently, the numerous problems mentioned in this article only surfaced after the proposed changes were officially released.)</span>
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
Go 1.22 release notes: <a href="https://go.dev/doc/go1.22">https://go.dev/doc/go1.22</a>, which claims the release maintains <a href="https://go.dev/doc/go1compat">the Go 1 promise of compatibility</a>. However, <span class="tmd-bold">this is simply not the case (read below for reasons).</span>
</div>
</li>
</ol>
<p></p>
<p></p>
<h2 id="impact" class="tmd-header-2">
The impact of the changes
</h2>
<p></p>
<div class="tmd-usual">
Personally, I think the rationale of the change to <code class="tmd-code-span">for-range</code> loops is well-justified. The new semantics of <code class="tmd-code-span">for-range</code> loops become more intuitive. The change only affects <code class="tmd-code-span">for k, v := range .. {...}</code> loops, in which the <code class="tmd-code-span">:=</code> symbol strongly suggests that the loop variables are per-iteration scoped. No implications are introduced. The impact of the change is almost positive.
</div>
<p></p>
<div class="tmd-usual">
On the other hand, in my honest opinion, the rationale of the change to <code class="tmd-code-span">for;;</code> loops is insufficient. The main reason provided by the proposal makers is to make a consistency with <code class="tmd-code-span">for-range</code> loops (they are both <code class="tmd-code-span">for</code> loops). However, It's not intuitive at all to think the loop variables in the following alike loops are per-iteration scoped.
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">for a, b, c := anExpression; aCondition; postStatement {
	... // loop body
}
</code></pre>
<p></p>
<div class="tmd-usual">
The <code class="tmd-code-span">a, b, c := anExpression</code> statement is only executed once during the execution of the loop, so it is intuitive that the loop variables are only <span class="tmd-bold"><span class="tmd-italic">explicitly</span></span> instantiated once during the execution of the loop. The new semantics make the the loop variables instantiated at each iteration, which means there must be some <span class="tmd-bold"><span class="tmd-italic">implicit</span></span><span class="tmd-italic"></span> code to do the job. This is true. <a href="https://go.dev/ref/spec#For_statements">Go 1.22+ specification</a> says:
</div>
<p></p>
<div class="tmd-quotation">
<div class="tmd-usual">
Each iteration has its own separate declared variable (or variables). The variable used by the first iteration is declared by the init statement. The variable used by each subsequent iteration is declared implicitly before executing the post statement and initialized to the value of the previous iteration's variable at that moment.
</div>
</div>
<p></p>
<p></p>
<div class="tmd-usual">
By the speficication, since Go 1.22, the loop shown above is actually equivalent to the following pseudo-code (<span class="tmd-italic">Sorry, the new semantics are hard to explain in a clear and perfect way. None of Go official documentations ever successfully achieve this goal. Here, I have tried my best.</span>):
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">{
	a_last, b_last, c_last := anExpression
	pa_last, pb_last, pc_last = &amp;a_last, &amp;b_last, &amp;c_last
	first := true
	for {
		a, b, c := *pa_last, *pb_last, *pc_last
		if first {
			first = false
		} else {
			postStatement
		}
		if !(aCondition) {
			break
		}
		pa_last, pb_last, pc_last = &amp;a, &amp;b, &amp;c
		... // loop body
	}
}
</code></pre>
<p></p>
<div class="tmd-usual">
Wow, quite a lot of magical implicit code. For a language that promotes explicitness, it's embarrassing.
</div>
<p></p>
<div class="tmd-usual">
Implicitness frequently results in unintended surprises, a fact that is itself unsurprising. The following will show several cases which might break your expectations.
</div>
<p></p>
<h3 id="defer-calls" class="tmd-header-3">
The behaviors of deferred function calls which capture loop variables might change
</h3>
<p></p>
<div class="tmd-usual">
A simple example:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-defer.go
package main

import "fmt"

func main() {
	for counter, n := 0, 2; n &gt;= 0; n-- {
		defer func(v int) {
		    fmt.Print("#", counter, ": ", v, "\n")
		    counter++
		}(n)
	}
}
</code></pre>
<p></p>
<div class="tmd-usual">
Its outputs:
</div>
<pre class="tmd-code">
$ gotv 1.21. run demo-defer.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-defer.go
#0: 0
#1: 1
#2: 2
$ gotv 1.22. run demo-defer.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-defer.go
#0: 0
#0: 1
#0: 2
</pre>
<p></p>
<div class="tmd-usual">
You can find that, since Go 1.22, the value of <code class="tmd-code-span">counter</code> is never effectively increased. Why? I'm sorry. As mentioned above, it is some hard to clearly explain the new semantics and I don't think I have the ability to do this. You may get it from the following equivalent code:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">func main() {
	counter_last, n_last := 0, 2
	p_counter_last, p_n_last := &amp;counter_last, &amp;n_last
	first := true
	for {
		counter, n := *p_counter_last, *p_n_last
		if (first) {
			first = false
		} else {
			n--
		}
		
		if !(n &gt;= 0) {
			break
		}
		p_counter_last, p_n_last = &amp;counter, &amp;n
		defer func(v int) {
			fmt.Print("#", counter, ": ", v, "\n")
			counter++
		}(n)
	}
}
</code></pre>
<p></p>
<div class="tmd-usual">
A more realistic example:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// search.go
package main

import "fmt"

func demoFilter(n int) bool {
	return n &amp; 1 == 0;
}

// Search values and return them without perverting order.
func search(start, end int)(r []int) {
	var count = 0
	for i, index := start, 0; i &lt;= end; i++ {
		if demoFilter(i) {
			count++
			defer func(value int) {
				r[index] = value
				index++
			}(i)
		}
	}
	
	r = make([]int, count) // only allocate once
	return
}

func main() {
	fmt.Println(search(0, 9))
}
</code></pre>
<p></p>
<div class="tmd-usual">
The outputs of the above program:
</div>
<pre class="tmd-code">
$ gotv 1.21. run search.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run search.go
[8 6 4 2 0]
$ gotv 1.22. run search.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run search.go
[0 0 0 0 0]
</pre>
<p></p>
<div class="tmd-usual">
So, since Go 1.22, just be careful when using freshly-declared loop variables in deferred function calls.
</div>
<p></p>
<div class="tmd-callout">
<div class="tmd-callout-content">
<div class="tmd-usual">
The above example suggests that some freshly-declared loop variables in the first clause of a <code class="tmd-code-span">for;;</code> loops might be okay to be per-iteration scoped, but some are strongly expected to be whole-loop scoped, such as the <code class="tmd-code-span">index</code> and <code class="tmd-code-span">counter</code> loop variables shown above. I ever suggested to <a href="https://github.com/golang/go/issues/60078#issuecomment-1547130632">allow re-declaration statements as <code class="tmd-code-span">postStatement</code> of <code class="tmd-code-span">for;;</code> loops to <span class="tmd-bold"><span class="tmd-italic">explicitly</span></span> specify which variables are per-iteration scoped</a>. For example, in the following loop code, <code class="tmd-code-span">n</code> is per-iteration scoped but <code class="tmd-code-span">counter</code> is whole-loop scoped.
</div>
<div class="tmd-base">
<p></p>
<pre class="tmd-code">
<code class="language-Go">  for counter, n := 0, 2; n &gt;= 0; n := n - 1 { ... }
</code></pre>
<p></p>
<div class="tmd-usual">
However, sadly, the suggestion was ignored totally.
</div>
</div>
</div>
</div>
<p></p>
<p></p>
<h3 id="capture-loop-vars" class="tmd-header-3">
Be careful when capturing loop variables in closures
</h3>
<p></p>
<div class="tmd-usual">
An example:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-closure-1.go
package main

import "fmt"

func main() {
	var printN func()
	for n := 0; n &lt; 9; n++ {
		if printN == nil {
			printN = func() {
				fmt.Println(n)
			}
		}
	}
	printN()
}
</code></pre>
<p></p>
<div class="tmd-usual">
Its outputs:
</div>
<pre class="tmd-code">
$ gotv 1.21. run demo-closure-1.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-closure-1.go
9
$ gotv 1.22. run demo-closure-1.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-closure-1.go
0
</pre>
<p></p>
<div class="tmd-usual">
Prior to Go 1.22, what the <code class="tmd-code-span">printN</code> closure captures is the only instance of the loop variable, which final value is <code class="tmd-code-span">9</code>. However, since Go 1.22, what the <code class="tmd-code-span">printN</code> closure captures is the first instance of the loop variable, which final value is <code class="tmd-code-span">0</code>. That is the reason of the behavior difference between the two Go versions.
</div>
<p></p>
<div class="tmd-usual">
Here is a similar example:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-closure-2.go
package main

import (
	"bytes"
	"fmt"
)

func main() {
	var printBuf func()
	for buf, i := (bytes.Buffer{}), byte('a'); i &lt;= 'z'; i++ {
		if printBuf == nil {
			printBuf = func() {
				fmt.Printf("%s\n", buf.Bytes())
			}
		}
		buf.WriteByte(i)
	}
	printBuf()
}
</code></pre>
<p></p>
<div class="tmd-usual">
Its outputs:
</div>
<pre class="tmd-code">
$ gotv 1.21. run demo-closure-2.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-closure-2.go
abcdefghijklmnopqrstuvwxyz
$ gotv 1.22. run demo-closure-2.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-closure-2.go
a
</pre>
<p></p>
<div class="tmd-usual">
The third example:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">package main

func main() {
    var next func()
    for i := 0; i &lt; 3; next() {
        print(i)
        next = func() {
            i++
        }
    }
}
</code></pre>
<p></p>
<div class="tmd-usual">
It will never exit since Go 1.22 (prior to Go 1.22, it prints <code class="tmd-code-span">012</code> then exits immediately)
</div>
<p></p>
<div class="tmd-callout">
<div class="tmd-callout-content">
<div class="tmd-usual">
So, whether you think it is intuitive or not, just remember that, since Go 1.22, a freshly-declared loop variable may have many instances at run time, whether or not it is modified in <code class="tmd-code-span">postStatement</code>. Each of the instances is instantiated in one iteration.
</div>
</div>
</div>
<p></p>
<h3 id="take-addresses-of-loop-vars" class="tmd-header-3">
Be careful when taking addresses of loop variables
</h3>
<p></p>
<div class="tmd-usual">
Similarly, since Go 1.22, it may be dangerous to use the address of a freshly-declared loop variable across loop iterations.
</div>
<p></p>
<div class="tmd-usual">
For example, what does the following Go program print? <span class="tmd-italic">(Some people say this example is so bizarre that backward-compatibility should not be kept for such cases. What a ridiculous point! The code in reality may be more bizarre than this!)</span>
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-pointer1.go
package main

import "fmt"

func main() {
	for i, p := 0, (*int)(nil); p == nil; fmt.Println(p == &amp;i) {
		p = &amp;i
	}
}
</code></pre>
<p></p>
<div class="tmd-usual">
Its outputs:
</div>
<pre class="tmd-code">
$ gotv 1.21. run demo-pointer1.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-pointer1.go
true
$ gotv 1.22. run demo-pointer1.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-pointer1.go
false
</pre>
<p></p>
<div class="tmd-usual">
Go 1.21 and 1.22 give different answers. Why? From the equivalent code shown below, we can get that, in the comparison <code class="tmd-code-span">p == &amp;i</code>, <code class="tmd-code-span">p</code> points to the first instance of <code class="tmd-code-span">i</code>, whereas <code class="tmd-code-span">&amp;i</code> takes the address of the second instance of <code class="tmd-code-span">i</code>. So the comparison evaluation result is <code class="tmd-code-span">false</code>.
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">func main() {
	i_last, p_last := 0, (*int)(nil)
	p_i_last, p_p_last := &amp;i_last, &amp;p_last
	first := true
	for {
		i, p := *p_i_last, *p_p_last
		if first {
			first = false
		} else {
			fmt.Println(p == &amp;i)
		}
		if !(p == nil) {
			break
		}
		p_i_last, p_p_last = &amp;i, &amp;p
		p = &amp;i
	}
}
</code></pre>
<p></p>
<div class="tmd-usual">
Another example:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-pointer2.go
package main

import "fmt"

func main() {
    var p *int
	for i := 0; i &lt; 3; *p++ {
	    p = &amp;i
	    fmt.Println(i)
	}
}
</code></pre>
<p></p>
<div class="tmd-usual">
Since Go 1.22, the above program will never exit (prior to Go 1.22, it will):
</div>
<pre class="tmd-code">
$ gotv 1.21. run demo-pointer2.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-pointer2.go
0
1
2
$ gotv 1.22. run demo-pointer2.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-pointer2.go
0
0
0
...
</pre>
<p></p>
<h3 id="place-3rd-clauses" class="tmd-header-3">
Be careful when moving the 3rd clause statements inside loop bodies
</h3>
<p></p>
<div class="tmd-usual">
Since Go 1.22, the following two loops might be not equivalent with each other any more (prior to Go 1.22, they are equivalent).
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">for ...; ...; postStatement {
	... // no continue statements here
}

for ...; ...; {
	... // no continue statements here
	postStatement
}
</code></pre>
<p></p>
<div class="tmd-usual">
For example, if we move the 3rd clause statements of the loops in the last section into loop bodies, then their behaviors change (since Go 1.22).
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-pointer3.go
package main

import "fmt"

func pointerDemo1() {
	for i, p := 0, (*int)(nil); p == nil; {
		p = &amp;i
		fmt.Println(p == &amp;i) // the old 3rd clause
	}
}


func pointerDemo2() {
    var p *int
	for i := 0; i &lt; 3; {
	    p = &amp;i
	    fmt.Println(i)
	    *p++ // the old 3rd clause
	}
}

func main() {
	pointerDemo1();
	pointerDemo2();
}
</code></pre>
<p></p>
<div class="tmd-usual">
The new outputs:
</div>
<pre class="tmd-code">
$ gotv 1.22. run demo-pointer3.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-pointer3.go
true
0
1
2
</pre>
<p></p>
<h3 id="nocopy-loop-vars" class="tmd-header-3">
Be careful when declaring no-copy values as loop variables
</h3>
<p></p>
<div class="tmd-usual">
As explained above, since Go 1.22, at the start of each loop iteration, each freshly-declared loop variable will get copied once, <span class="tmd-bold"><span class="tmd-italic">implicitly</span></span>. The implication means that, since Go 1.22, it’s generally not advisable to use no-copy values as loop variables. Examples include <code class="tmd-code-span">sync.Mutex</code>, <code class="tmd-code-span">sync/atomic.Int64</code>, <code class="tmd-code-span">bytes.Buffer</code>, <code class="tmd-code-span">strings.Builder</code>, and <code class="tmd-code-span">container/list.List</code>, etc.
</div>
<p></p>
<div class="tmd-usual">
For example, in Go versions prior to 1.22, the following code was considered concurrently correct. However, starting with Go 1.22, this code is considered to have a concurrency issue, because the loop variable <code class="tmd-code-span">wg</code> will be (implicitly) copied at the start of each loop iteration.
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-nocopy1.go
package main

import (
	"sync"
	"time"
)

func process() (wait func()) {
	for wg, i := (sync.WaitGroup{}), 0; i &lt; 3; i++ {
		if (wait == nil) {
			wait = wg.Wait
		}
		
		wg.Add(1)
		go func(v int) {
			defer wg.Done()
			if (v &gt; 0) {
				time.Sleep(time.Second/8)
			}
			println(v)
		}(i)
	}
	return
}

func main() {
	process()()
}
</code></pre>
<p></p>
<div class="tmd-usual">
Its outputs:
</div>
<pre class="tmd-code">
$ gotv 1.21. run demo-nocopy1.go
[Run]: $HOME/.cache/gotv/tag_go1.21.8/bin/go run demo-nocopy1.go
0
2
1
$ gotv 1.22. run demo-nocopy1.go
[Run]: $HOME/.cache/gotv/tag_go1.22.1/bin/go run demo-nocopy1.go
0
$ gotv 1.22. vet demo-nocopy1.go
[Run]: $HOME/.cache/gotv/tag_go1.22.1/bin/go vet demo-nocopy1.go
</pre>
<p></p>
<div class="tmd-usual">
Note that the <code class="tmd-code-span">go vet</code> command in Go toolchain versions prior to 1.24 can't catch such implicit duplication of no-copy values, regardless of whether the loop variable <code class="tmd-code-span">wg</code> is captured in the loop body or not.
</div>
<p></p>
<div class="tmd-usual">
Certain <code class="tmd-code-span">no-copy</code> checks occur during run time. The <code class="tmd-code-span">go vet</code> command in Go toolchain v1.24+ still can't catch such cases. Let's view an example which uses <code class="tmd-code-span">strings.Builder</code> (each <code class="tmd-code-span">strings.Builder</code> value contains a pointer field which should point to itself):
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-nocopy2.go
package main

import "strings"
import "fmt"

func foo(pb *strings.Builder) {}

var bar = foo

func a2z_foo() string {
	for b, i := (strings.Builder{}), byte('a'); ; i++ {
		b.WriteByte(i)
		foo(&amp;b) // &lt;- difference is here
		if i == 'z' {
			return b.String()
		}
	}
}

func a2z_bar() string {
	for b, i := (strings.Builder{}), byte('a'); ; i++ {
		b.WriteByte(i)
		bar(&amp;b) // &lt;- difference is here
		if i == 'z' {
			return b.String()
		}
	}
}

func main() {
	fmt.Println("foo:", a2z_foo())
	fmt.Println("bar:", a2z_bar())
}
</code></pre>
<p></p>
<div class="tmd-usual">
Run it with different Go toolchains, we get:
</div>
<pre class="tmd-code">
$ gotv 1.21. run demo-nocopy2.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run demo-nocopy2.go
foo: abcdefghijklmnopqrstuvwxyz
bar: abcdefghijklmnopqrstuvwxyz
$ gotv 1.22. run demo-nocopy2.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run demo-nocopy2.go
foo: abcdefghijklmnopqrstuvwxyz
panic: strings: illegal use of non-zero Builder copied by value

goroutine 1 [running]:
...
</pre>
<p></p>
<div class="tmd-usual">
The outputs show that:
</div>
<ul class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
prior to Go 1.22, the behaviors of the two <code class="tmd-code-span">a2z</code> functions are consistent. No loop variables duplication happens, so there will be no panic.
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
since Go 1.22, as fresh-declared loop variables implicitly get copied at the start of each loop iteration, the run-time <code class="tmd-code-span">no-copy</code> check functions. But the check only works for the <code class="tmd-code-span">a2z_bar</code> function, not for the <code class="tmd-code-span">a2z_foo</code> function.
</div>
</li>
</ul>
<p></p>
<div class="tmd-usual">
But why doesn't the run-time <code class="tmd-code-span">no-copy</code> check work for the <code class="tmd-code-span">a2z_foo</code> function? Because of two compiler optimizations. In one optimization, the compiler omits the <code class="tmd-code-span">foo(&amp;b)</code> line in the <code class="tmd-code-span">a2z_foo</code> function. The optimization is actually valid. The optimization is conservative so that the compiler doesn't omits the <code class="tmd-code-span">bar(&amp;b)</code> line in the <code class="tmd-code-span">a2z_bar</code> function. The consequence is, in the other optimization, the compiler (mistakenly) thinks the fresh-declared loop variable <code class="tmd-code-span">b</code> in the <code class="tmd-code-span">a2z_foo</code> function can be instantiated only once for the entire loop, even though its semantics suggest per-iteration instantiation.
</div>
<p></p>
<div class="tmd-usual">
However, the compiler is too smart to <a href="https://github.com/golang/go/issues/66070">make a bad decision</a> here. The compiler incorrectly implements the semantics. The behaviors of the two <code class="tmd-code-span">a2z</code> functions in the above program should be still consistent since Go 1.22. They should both panic.
</div>
<p></p>
<div class="tmd-usual">
The Go core team <a href="https://github.com/golang/go/issues/66070#issuecomment-1981642904">refused to fix this bug</a>, which raises the alarm that unexpected behavior may happen in other similar scenarios.
</div>
<p></p>
<p></p>
<div class="tmd-usual">
The <code class="tmd-code-span">go vet</code> command in the latest Go toolchain versions still can't catch such implicit duplication of no-copy values.
</div>
<p></p>
<div class="tmd-usual">
Note: in the standard library, there are some other types, such as the <code class="tmd-code-span">bytes.Buffer</code> type, which values should also not be copied. However, neither compile-time checks or run-time checks are made for them.
</div>
<p></p>
<div class="tmd-usual">
The safe advice is try not to declare no-copy values as loop variables. This is just a suggestion, not a mandatory rule, because copying no-copy values does not always cause damage (but the damage may be exposed later when the code is refactored in some way).
</div>
<p></p>
<h3 id="large-size-loop-vars" class="tmd-header-3">
Warning: the performance of your Go programs might be degraded silently
</h3>
<p></p>
<div class="tmd-usual">
Sometimes, a compiler is over smart; sometimes, it is not smart enough. For example, sometimes, the official standard compiler provided in Go toolchain 1.22 is unable to determine that each instance of a freshly-declared loop variable is used solely within the corresponding iteration's lifetime, so that the loop variable will be instantiated per iteration and each of its instances will be allocated on heap instead of stack. Even worse, if the size of the loop variable is large, then high duplication costs will be incurred. When these situations occur, the performance of the program will be degraded.
</div>
<p></p>
<div class="tmd-usual">
Let's view an example, in which a large-size loop variable is used in the <code class="tmd-code-span">bar</code> function.
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-largesize.go
package main

import (
	"fmt"
	"time"
)

const N = 1 &lt;&lt; 18

func foo(f func([]byte, int)) {
	a := [N]byte{}
	for i := 0; i &lt; len(a); i++ {
		f(a[:], i)
	}
}

func bar(f func([]byte, int)) {
	for a, i := [N]byte{}, 0; i &lt; len(a); i++ {
		f(a[:], i)
	}
}

func main() {
	readonly := func(x []byte, k int) {}
	bench := func(f func(func([]byte, int))) time.Duration {
		start := time.Now()
		f(readonly)
		return time.Since(start)
	}
	fmt.Println("foo time:", bench(foo))
	fmt.Println("bar time:", bench(bar))
}
</code></pre>
<p></p>
<div class="tmd-usual">
Its outputs:
</div>
<pre class="tmd-code">
$ gotv 1.21. run aaa.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run aaa.go
foo time: 689.79µs
bar time: 690.988µs
$ gotv 1.22. run aaa.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run aaa.go
foo time: 734.85µs
bar time: 18.795043596s
</pre>
<p></p>
<div class="tmd-usual">
The benchmark results reveal a significant performance regression in the <code class="tmd-code-span">bar</code> function between Go 1.21 and 1.22 (note that 1s == 1,000,000us). Why? Because, with the official standard Go compiler 1.22, the loop variable <code class="tmd-code-span">a</code> in the <code class="tmd-code-span">bar</code> function is duplicated in each iteration. Whereas in prior versions, such duplication is always needless.
</div>
<p></p>
<div class="tmd-usual">
The performance degradation problem does not affect the correctness of the code logic (at least for the above specified example). And the degree of performance degradation depends on the value size of the array loop variable. Therefore, it might not be detected in time for some Go projects when they are upgraded from a version prior to 1.22 and to a 1.22+ version.
</div>
<p></p>
<div class="tmd-usual">
Suggestions to avoid such performance degradation issue:
</div>
<ol class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
Try not to declare large-size values as loop variables, even if the syntax allows to do so.
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
In certain situations, consider declaring loop variables outside the loop itself to optimize performance. This is beneficial if you can guarantee that the variables don't need to be instantiated in each iteration.
</div>
</li>
</ol>
<p></p>
<h3 id="sharing-loop-vars" class="tmd-header-3">
Warning: things might become more subtle than before when loop variables are used concurrently
</h3>
<p></p>
<div class="tmd-usual">
Firstly, let's view a simple program.
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-concurency1.go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	for i := 0; i &lt; 3; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println(i)
		}()
	}
	wg.Wait()
}
</code></pre>
<p></p>
<div class="tmd-usual">
The above program is intended to print the values of the loop variable <code class="tmd-code-span">i</code> at each iteration. Prior to Go 1.22, there is a clear data race condition present in the program, because the loop variable <code class="tmd-code-span">i</code> is only instantiated once during the whole loop. All the new created goroutines will read the single instance but the main goroutine will modify it. The following outputs prove this fact:
</div>
<pre class="tmd-code">
$ CGO_ENABLED=1 gotv 1.21. run -race demo-concurency1.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run -race demo-concurency1.go
3
3
==================
WARNING: DATA RACE
...
==================
3
</pre>
<p></p>
<div class="tmd-usual">
Prior to Go 1.22, the fix is simple, just add an <code class="tmd-code-span">i := i</code> line at the start of the loop body. Go 1.22 fixes the specified data race problem by changing the semantics of <code class="tmd-code-span">for;;</code> loops, without modifying the old problematic code. This can be verified by the following outputs:
</div>
<pre class="tmd-code">
$ CGO_ENABLED=1 gotv 1.22. run -race demo-concurency1.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run -race demo-concurency1.go
1
2
0
</pre>
<p></p>
<div class="tmd-usual">
In fact, this is just the reason why Go 1.22 made the semantic change to <code class="tmd-code-span">for;;</code> loops. But is it worth it to fix such a small problem by introducing magical implicit code?
</div>
<p></p>
<div class="tmd-usual">
The effect of the attempt to fix the problem by making semantic change is actually limited. Let's modify the above program a bit:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-concurency2.go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	for i := 0; i &lt; 3; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			i++ // &lt;-- add this line
			fmt.Println(i)
		}()
	}
	wg.Wait()
}
</code></pre>
<p></p>
<div class="tmd-usual">
Is the new code still data race free (with Go 1.22 semantics)? It looks good. Each new created goroutine just uses an exclusive copy of the loop variable <code class="tmd-code-span">i</code>. But the answer is "no", because there is an implicit assignment at the start of each iteration and the implicit assignment uses an instance of the loop variable as source value (a.k.a. the main goroutine reads it), however the instance is modified in a new created goroutine.
</div>
<p></p>
<div class="tmd-usual">
The following outputs verify there is a data race condition present in the new code:
</div>
<pre class="tmd-code">
$ CGO_ENABLED=1 gotv 1.22. run -race demo-concurency2.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run -race demo-concurency2.go
==================
WARNING: DATA RACE
...
==================
2
1
3
Found 1 data race(s)
</pre>
<p></p>
<div class="tmd-usual">
Prior to Go 1.22, the data race is clear and easily to detect. But since Go 1.22, things become more subtle and the data race is not very clear (because of the implicit code).
</div>
<p></p>
<div class="tmd-usual">
You may have found that the data-racy program appears to produce the intended outputs. Yes, this is another problem of the semantic change. While the data-racy program's outputs may appear correct most of the time, this masks a deeper issue: the data race can remain undetected for a longer period. This can significantly delay the identification and resolution of the problem!
</div>
<p></p>
<div class="tmd-usual">
More seriously, some old good concurrent code will become problematic. Here is an example:
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">// demo-concurency3.go
package main

import (
	"fmt"
	"sync"
)

const NumWorkers = 3

func isGold(num uint64) bool {
	return num &amp; 0xFFFFF == 0
}

func main() {
	var c = make(chan uint64)
	var m sync.Mutex
	for n, i := 0, uint64(0); n &lt; NumWorkers; n++ {
		go func() {
			for {
				m.Lock()
				i++
				v := i
				m.Unlock()
				
				if isGold(v) {
					c &lt;- v
				}
			}
		}()
	}
	
	for n := range c {
		fmt.Println("Found gold", n)
	}
}
</code></pre>
<p></p>
<div class="tmd-usual">
Run it with different toolchain versions, get the following outputs:
</div>
<pre class="tmd-code">
$ CGO_ENABLED=1 gotv 1.21. run -race demo-concurency3.go
[Run]: $HOME/.cache/gotv/tag_go1.21.7/bin/go run -race demo-concurency3.go
Found gold 1048576
Found gold 2097152
Found gold 3145728
...
^C
$ CGO_ENABLED=1 gotv 1.22. run -race demo-concurency3.go
[Run]: $HOME/.cache/gotv/tag_go1.22.0/bin/go run -race demo-concurency3.go
==================
WARNING: DATA RACE
...
==================
Found gold 1048576
Found gold 1048576
Found gold 1048576
Found gold 2097152
Found gold 2097152
Found gold 2097152
Found gold 3145728
Found gold 3145728
Found gold 3145728
...
^C
</pre>
<p></p>
<div class="tmd-usual">
😳😳😳... <span class="tmd-italic">(Considering that the title of the proposal to make the semantic change is "<mark class="tmd-marked-2">Proposal: Less Error-Prone Loop Variable Scoping</mark>". Is it irony?)</span>
</div>
<p></p>
<div class="tmd-usual">
How to fix the (since-1.22-)data-racy code?
</div>
<ul class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
To merely avoid the race condition, we can still use the old trick: just add an <code class="tmd-code-span">i := i</code> line at the start of the loop body. Yes, this is still the best suggestion to avoid data race for such situations in the Go 1.22+ era. Is this a mockery of the intention of the semantic change (of <code class="tmd-code-span">for;;</code> loops)?
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
To avoid the race condition and correct the logic, the variable <code class="tmd-code-span">i</code> should be declared outside the loop block (since Go 1.22).
</div>
</li>
</ul>
<p></p>
<h2 id="advice" class="tmd-header-2">
Advice and suggestions
</h2>
<p></p>
<div class="tmd-usual">
Okay, the above are the potential issues I've identified so far with the new <code class="tmd-code-span">for;;</code> semantics introduced in Go 1.22. There might be more, I'm not sure.
</div>
<p></p>
<div class="tmd-usual">
Here are some recommendations you can follow in the Go 1.22+ era.
</div>
<p></p>
<h3 id="file-go-versions" class="tmd-header-3">
Specify Go language versions for Go source files
</h3>
<p></p>
<div class="tmd-usual">
As demonstrated in many above examples, the semantic changes made in Go 1.22 break backward-compatibility. To reduce the damage as small as possible, Go core team decided to associate a Go language version to every Go source file. In other words, since Go 1.22, <mark class="tmd-marked">a Go source file</mark> = <mark class="tmd-marked">code</mark> + <mark class="tmd-marked">Go language version</mark>.
</div>
<p></p>
<div class="tmd-usual">
There are several ways (in order of priority) to specify Go language versions for Go source files:
</div>
<ol class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
Add a <code class="tmd-code-span">//go:build go1.xy</code> comment directive at the start of a source file to specify the language version for the source file. <span class="tmd-bold"><span class="tmd-italic">(However, the <code class="tmd-code-span">//go:build go1.xy</code> comment directive in a Go source file might still be ignored due to potential bugs in the toolchain implementation. For example, the Go core team is not willing to fix </span></span><a href="https://github.com/golang/go/issues/66092"><span class="tmd-bold"><span class="tmd-italic">this known bug</span></span></a><span class="tmd-bold"><span class="tmd-italic"> for Go toolchain v1.22.x versions.)</span></span>
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
Use <code class="tmd-code-span">-gcflags=-lang=go1.xy</code> compiler option when building a Go binary to specify the Go language version for the seed files you're passing to the compiler.
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
Put a <code class="tmd-code-span">go 1.xy</code> directive line in the <code class="tmd-code-span">go.mod</code> file of a Go module to specify the default Go language version for all of the source files in the Go module. A missing go directive line is assumed as <code class="tmd-code-span">go 1.16</code>. The effects of the directive line are <a href="https://go.dev/ref/mod#go-mod-file-go">described here</a>.
</div>
</li>
</ol>
<p></p>
<p></p>
<div class="tmd-usual">
If the Go language version of a Go source file is not specified by any of the above ways, then the version of the used Go compiler is used. In other words, the behavior of the code in the source file is compiler dependent.
</div>
<p></p>
<div class="tmd-usual">
The design causes two problems:
</div>
<ol class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
Some people like to use the <code class="tmd-code-span">go run</code> command to run Go code as scripts (a set of Go source files without <code class="tmd-code-span">go.mod</code> files). If a Go script source file doesn't contain a <code class="tmd-code-span">//go:build go1.xy</code> comment directive and the <code class="tmd-code-span">-gcflags=-lang=go1.xy</code> compiler option is not specified, then the behavior of the code in the script file is compiler dependent, just as what the above examples show.
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
If, for any reason (for example, to use a feature provided in newer Go versions), you upgrade the language version of a Go module that you maintain to 1.22 or higher, the behavior of the Go module might change. If the behavior changes are not detected in time (due to insufficient testing and the drawbacks of the new senmantics of <code class="tmd-code-span">for;;</code> loops, etc.), then things may not go well.
</div>
</li>
</ol>
<p></p>
<div class="tmd-usual">
Anyway, since Go 1.22, you should try to specify a Go language version for every Go source file, in any of the above introduced ways, to reduce compiler version dependent behaviors. This is the minimum standard to be a professional Go programmer in the Go 1.22+ era.
</div>
<p></p>
<h3 id="notes-during-upgrading" class="tmd-header-3">
Upgrading module versions
</h3>
<p></p>
<div class="tmd-usual">
It is a sad fact that <span class="tmd-larger-size"><span class="tmd-italic"><span class="tmd-bold">the proposal (and trouble) makers never intend to to provide automatic ways to identify potential problems when upgrading module versions</span></span></span>. So you must identify potential problems manually.
</div>
<p></p>
<ul class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
If you are maintaining a public Go module which are depended by other Go projects, please carefully check all the uses of <code class="tmd-code-span">for;;</code> loops in the module's code before bumping the language version to Go 1.22+ in the go.mod file. especially pay attention to those freshly-declared loop variables which are not modified in <code class="tmd-code-span">postStatement</code>, to avoid breaking the projects depending on this module.
</div>
<p></p>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
If you upgrade dependency modules of your Go projects, pay attention to those ones which language versions are upgraded to Go 1.22 or higher from a version with the old semantics before Go 1.22, to avoid breaking your projects.
</div>
<div class="tmd-base">
<p></p>
<div class="tmd-usual">
And please note that, <span class="tmd-bold"><span class="tmd-italic">sometimes, the <code class="tmd-code-span">go mod tidy</code> command provided by the official Go toolchain will automatically upgrade the language versions (specified in <code class="tmd-code-span">go.mod</code> files) of your projects, without detecting potential problems that will break the behaviors of your projects, without notifying you the upgradation. You might find the behavior changes in time or not. Just be careful!</span></span>
</div>
</div>
</li>
</ul>
<p></p>
<h3 id="avoid-freshly-declared-loop-vars" class="tmd-header-3">
Avoid using freshly-declared loop variables in <code class="tmd-code-span">for;;</code> loops if you worry about getting bitten by the pitful of the new semantics
</h3>
<p></p>
<div class="tmd-usual">
Don't be too nervous, :D. In fact, most <code class="tmd-code-span">for;;</code> loops behave the same with either the old semantics or the new semantics. But if you're unsure about it, you can always rewrite the following alike loops
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">for a, b, c := anExpression; aCondition; postStatement {
	... // loop body
}
</code></pre>
<p></p>
<div class="tmd-usual">
as
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">{
	a, b, c := anExpression
	for ; aCondition; postStatement {
		... // loop body
	}
}
</code></pre>
<p></p>
<div class="tmd-usual">
to avoid triggering the new semantics. You can even specify which loop variables are instantiated per loop iteration and which are not. For example, in the following code, <code class="tmd-code-span">a</code> and <code class="tmd-code-span">c</code> are instantiated per loop iteration, but <code class="tmd-code-span">b</code> will be only instantiated once during the whole loop.
</div>
<p></p>
<pre class="tmd-code">
<code class="language-Go">{
	a, b, c := anExpression
	for ; aCondition; postStatement {
		a, c := a, c
		... // loop body
	}
}
</code></pre>
<p></p>
<div class="tmd-usual">
This is a little awkward, but it is much safer.
</div>
<p></p>
<h2 id="final-words" class="tmd-header-2">
Final words
</h2>
<p></p>
<div class="tmd-usual">
Overall, I find the impact of the new semantics of <code class="tmd-code-span">for-range</code> loops is positive, while the impact of the new semantics of <code class="tmd-code-span">for;;</code> loops is negative. In my honest opinion, the benefits of the new semantics of <code class="tmd-code-span">for;;</code> loops are rare and tiny, whereas the drawbacks are more prominent and serious.
</div>
<p></p>
<div class="tmd-usual">
The semantic change damaged Go's reputation for promoting explicitness and maintaining strong backward compatibility.
</div>
<p></p>
<div class="tmd-usual">
Because of the introduced magical implicitness, the new semantics of <code class="tmd-code-span">for;;</code> loops might require additional debug time in code writing and additional cognitive effort during code review and understanding in some cases.
</div>
<p></p>
<div class="tmd-usual">
The new semantics of <code class="tmd-code-span">for;;</code> loops might introduce potential performance degradation and data race issues in existing code, requiring careful review and potential adjustments. Depending on specific cases, such issues might be found in time or not.
</div>
<p></p>
<div class="tmd-usual">
The semantic changes introduced in Go 1.22 significantly lower the standard for maintaining backward compatibility. This is a bad start.
</div>
<p></p>
<div class="tmd-usual">
I have expressed my opinions in the following comments:
</div>
<ul class="tmd-list">
<li class="tmd-list-item">
<div class="tmd-usual">
<a href="https://github.com/golang/go/issues/60078#issuecomment-1541407014">https://github.com/golang/go/issues/60078#issuecomment-1541407014</a>
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
<a href="https://github.com/golang/go/issues/60078#issuecomment-1544324607">https://github.com/golang/go/issues/60078#issuecomment-1544324607</a>
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
<a href="https://github.com/golang/go/issues/60078#issuecomment-1547130632">https://github.com/golang/go/issues/60078#issuecomment-1547130632</a>
</div>
</li>
<li class="tmd-list-item">
<div class="tmd-usual">
<a href="https://github.com/golang/go/issues/60078#issuecomment-1558730767">https://github.com/golang/go/issues/60078#issuecomment-1558730767</a>
</div>
</li>
</ul>
<p></p>
<div class="tmd-usual">
But the proposal makers (some members in the Go core team) totally ignored them and decided to proceed with the semantic change of <code class="tmd-code-span">for;;</code> loops anyway.
</div>
<p></p>
<div class="tmd-usual">
What's done is done. In the end, I hope this article will help you write professional Go code in the Go 1.22+ era.
</div>
<p></p>
<hr  class="tmd-seperator"/>
<p></p>

<ol class="tmd-list tmd-footnotes" id="fn:">
<li id="fn:gotv" class="tmd-list-item tmd-footnote-item">
<div id="gotv" class="tmd-usual">
GoTV (<a href="https://go101.org/apps-and-libs/gotv.html">https://go101.org/apps-and-libs/gotv.html</a>) is a tool used to install and use coexisting installations of Go toolchain verisons harmoniously.
</div>
<a href="#fn:gotv:ref-1">↩︎</a></li>
</ol>
</div>
